<?php
#
# dmBridge: a data access framework for CONTENTdm(R)
#
# Copyright © 2009, 2010, 2011 Board of Regents of the Nevada System of Higher
# Education, on behalf of the University of Nevada, Las Vegas
#

/**
 * Takes in user input via the URI query string and makes it available via
 * convenient accessors.
 *
 * @author Alex Dolski <alex.dolski@unlv.edu>
 * @license http://www.opensource.org/licenses/mit-license.php
 * @todo Merge DMInput into DMObjectQuery
 */
class DMInput {

	private static $valid_date_qualifiers = array("from", "after", "before",
		"on");

	/** @var DMDateTime */
	private $begin_date;
	/** @var array */
	private $collections = array();
	/** @var string One of self::$valid_date_qualifiers */
	private $date_qualifier;
	/** @var DMDateTime */
	private $end_date;
	/** @var array of search terms */
	private $input_terms = array();
	/** @var boolean */
	private $is_present = false;
	/** @var DMDateTime */
	private $on_date;
	/** @var int */
	private $proximity;
	/** @var the URI containing input */
	private $uri;


	/**
	 * @param DMURI uri URI containing a query
	 */
	public function __construct(DMURI $uri) {
		$this->uri = $uri;
		$this->assembleUserInput();
	}

	private function assembleUserInput() {
		$qt = array();
		// simple search input (&q=...)
		if ($this->uri->getQueryValue("q")) {
			$qt[0]['string'] = $this->uri->getQueryValue("q");
			$qt[0]['field'] = "any";
			$qt[0]['mode'] = "all";
			$this->collections = DMCollection::getAuthorized();
			if (count($this->collections) < 1) {
				$this->collections[] = DMCollectionFactory::getCollection("/dmdefault");
			}
		} else { // CONTENTdm search (&CISOBLAH=...)
			for ($i = 1; $i < 5; $i++) {
				if ($this->uri->getQueryValue("CISOBOX" . $i)) {
					$qt[$i]['string'] = $this->uri->getQueryValue("CISOBOX" . $i);
				} else {
					break;
				}

				if ($this->uri->getQueryValue("CISOFIELD" . $i)) {
					$qt[$i]['field'] = $this->uri->getQueryValue("CISOFIELD" . $i);
				} else {
					break;
				}

				if ($this->uri->getQueryValue("CISOOP" . $i)) {
					$qt[$i]['mode'] = $this->uri->getQueryValue("CISOOP" . $i);
				} else {
					break;
				}
			} // for
			if ($this->uri->getQueryValue("CISOROOT")) {
				// CISOROOT may be a comma-delimited string, or an array.
				$aliases = array();
				if (is_array($this->uri->getQueryValue("CISOROOT"))) {
					$aliases = $this->uri->getQueryValue("CISOROOT");
				} else {
					$aliases = explode(",",
							$this->uri->getQueryValue("CISOROOT"));
				}
				if (in_array("all", $aliases)) {
					$this->collections[] = DMCollectionFactory::getCollection("/dmdefault");
				} else {
					foreach ($aliases as $a) {
						if ($a) {
							$this->collections[] = DMCollectionFactory::getCollection($a);
						}
					}
				}
			} // if
		} // else

		// check for presence of CISOROOT, CISOFIELD, CISOOP, or CISOBOX
		foreach ($this->uri->getQuery() as $kv) {
			if (substr($kv['key'], 0, 8) == "CISOROOT"
					|| substr($kv['key'], 0, 9) == "CISOFIELD"
					|| substr($kv['key'], 0, 6) == "CISOOP"
					|| substr($kv['key'], 0, 7) == "CISOBOX") {
				$this->is_present = true;
				break;
			}
		}

		foreach ($qt as $t) {
			if (!array_key_exists("field", $t)) {
				continue;
			}
			if (!array_key_exists("mode", $t)) {
				continue;
			}
			$f = new DMDCElement($t['field']);
			$f->setCollection(DMCollectionFactory::getCollection("/dmdefault"));
			$term = new DMQueryPredicate();
			$term->setString($t['string']);
			$term->setField(new DMDCElement($f->getNick()));
			$term->setMode($t['mode']);
			if ($term->isValid()) {
				$this->input_terms[] = $term;
			}
		}

		// date stuff
		if ($this->uri->getQueryValue("date_qualifier")) {
			$this->setDateQualifier(
					substr($this->uri->getQueryValue("date_qualifier"), 0, 6));
		}
		if ($this->uri->getQueryValue("proximity")) {
			$this->setProximity(
					(int) substr($this->uri->getQueryValue("proximity"), 0, 3));
		}

		if (in_array($this->getDateQualifier(), array("from", "after"))) {
			if (strlen($this->uri->getQueryValue("y1")) > 0
					&& strlen($this->uri->getQueryValue("y1")) <= 8) {
				$this->setBeginDate(
					new DMDateTime(sprintf("%d-%d-%d",
							$this->uri->getQueryValue("y1"),
							$this->uri->getQueryValue("m1"),
							$this->uri->getQueryValue("d1"))));
			}
		}
		if (in_array($this->getDateQualifier(), array("from", "before"))) {
			if (strlen($this->uri->getQueryValue("y2")) > 0
					&& strlen($this->uri->getQueryValue("y2")) <= 8) {
				$this->setEndDate(
					new DMDateTime(sprintf('%d-%d-%d',
							$this->uri->getQueryValue("y2"),
							$this->uri->getQueryValue("m2"),
							$this->uri->getQueryValue("d2"))));
			}
		}
		if ($this->getDateQualifier() == "on") {
			if (strlen($this->uri->getQueryValue("y1")) > 0
					&& strlen($this->uri->getQueryValue("y1")) <= 8) {
				$this->setOnDate(
					new DMDateTime(sprintf('%d-%d-%d',
							$this->uri->getQueryValue("y1"),
							$this->uri->getQueryValue("m1"),
							$this->uri->getQueryValue("d1"))));
			}
		}
	}

	/**
	 * @return Boolean
	 */
	public function isPresent() {
		return $this->is_present;
	}

	/**
	 * @return DMDateTime
	 * @since 0.1
	 */
	public function getBeginDate() {
		return $this->begin_date;
	}

	private function setBeginDate(DMDateTime $date) {
		$this->begin_date = $date;
	}

	/**
	 * @return Array of DMCollection objects
	 * @since 0.1
	 */
	public function getCollections() {
		return $this->collections;
	}

	/**
	 * @return string
	 * @since 0.1
	 */
	public function getDateQualifier() {
		return $this->date_qualifier;
	}

	private function setDateQualifier($q) {
		$valid = self::$valid_date_qualifiers;
		$valid[] = null;
		if (!in_array($q, $valid)) {
			throw new DMIllegalArgumentException(
				sprintf(DMLocalizedString::getString("INVALID_DATE_QUALIFIER"),
					implode(", ", self::$valid_date_qualifiers)));
		}
		$this->date_qualifier = $q;
	}

	/**
	 * @return DMDateTime
	 * @since 0.1
	 */
	public function getEndDate() {
		return $this->end_date;
	}

	private function setEndDate(DMDateTime $date) {
		$this->end_date = $date;
	}

	/**
	 * @return DMDateTime
	 * @since 0.1
	 */
	public function getOnDate() {
		return $this->on_date;
	}

	private function setOnDate(DMDateTime $date) {
		$this->on_date = $date;
	}

	/**
	 * @return int
	 */
	public function getProximity() {
		return $this->proximity;
	}

	private function setProximity($int) {
		$this->proximity = $int;
	}

	/**
	 * @param int index
	 * @return DMQueryPredicate
	 * @since 0.3
	 */
	public function getPredicate($index) {
		$terms = $this->getTerms();
		if ($index + 1 > count($terms)) {
			return false;
		}
		return ($terms[$index] instanceof DMQueryPredicate)
			? $terms[$index] : null;
	}

	/**
	 * @return array Array of DMQueryPredicate objects
	 * @since 0.1
	 */
	public function getTerms() {
		$begin_date = "00000000";
		if ($this->getBeginDate() instanceof DMDateTime) {
			$begin_date = $this->getBeginDate()->format("Ymd");
		}

		$end_date = "99999999";
		if ($this->getEndDate() instanceof DMDateTime) {
			$end_date = $this->getEndDate()->format("Ymd");
		}

		$terms = array();
		foreach ($this->input_terms as $t) {
			$terms[] = clone($t);
		}

		if (count($terms) < 1) {
			return array();
		}

		$on_date = null;
		if ($this->getOnDate() instanceof DMDateTime) {
			$on_date = $this->getOnDate()->format("Ymd");
		}

		// date search
		if (!($begin_date == "00000000" && $end_date == "99999999"
				&& empty($on_date))) {
			if (!$this->collections[0] instanceof DMCollection) {
				return array();
			}
			$nick = $this->collections[0]->getDateSearchField()->getNick();
			$m = "exact";
			$s = ($on_date) ? $on_date : $begin_date . "-" . $end_date;
			if ($terms[1] instanceof DMQueryPredicate) {
				$terms[1]->setDate(true);
				$terms[1]->setString($s);
				$terms[1]->setField(new DMDCElement($nick));
				$terms[1]->setMode($m);
			} else {
				$term = new DMQueryPredicate();
				$term->setString($s);
				$term->setField(new DMDCElement($nick));
				$term->setMode($m);
				$term->setDate(true);
				$terms[] = $term;
			}
		} else if ($this->getProximity() > 0) { // proximity search
			$s = sprintf("%s near%d %s",
				$terms[0]->getString(),
				$this->getProximity(),
				$terms[1]->getString()
			);
			$m = "exact";
			$term = new DMQueryPredicate();
			$term->setString($s);
			$term->setField(new DMDCElement("any"));
			$term->setMode($m);
			$term->setProximity(true);
			$terms = array($term);
		}

		return $terms;
	}

	/**
	 * @return Boolean
	 * @throws DMIllegalArgumentException
	 * @since 0.1
	 */
	public function validate() {
		// if no date is set, make sure all fields have been filled in
		if (!$this->getBeginDate() instanceof DMDateTime
				&& !$this->getEndDate() instanceof DMDateTime
				&& !$this->getOnDate() instanceof DMDateTime) {
			$i = 0;
			foreach ($this->getTerms() as $t) {
				if (strlen($t->getString()) < 1) {
					$i++;
				}
			}
			if ($i == count($this->getTerms())) {
				throw new DMIllegalArgumentException(
					DMLocalizedString::getString("MISSING_SEARCH_TERM"));
			}
		}
		// make sure end date is always >= begin date
		if ($this->getBeginDate() instanceof DMDateTime
				&& $this->getEndDate() instanceof DMDateTime) {
			if ($this->getBeginDate() > $this->getEndDate()) {
				throw new DMIllegalArgumentException(
					DMLocalizedString::getString("FROM_DATE_LATER"));
			}
		}
		// if a date is chosen...
		if ($this->getBeginDate() instanceof DMDateTime
				|| $this->getEndDate() instanceof DMDateTime) {
			// make sure there is only one term
			// (count is 2 because the date is always the 2nd term)
			if (count($this->getTerms()) > 2) {
				throw new DMIllegalArgumentException(
					DMLocalizedString::getString("MAX_TERMS_IN_DATE_SEARCH"));
			}
			// make sure a specific field is being searched (not "any")
			foreach ($this->getTerms() as $t) {
				if ($t->getField()->getNick() == "any") {
					throw new DMIllegalArgumentException(
						DMLocalizedString::getString("ANY_FIELD_IN_DATE_SEARCH"));
				}
			}
		}
		// if 'none' is selected as a mode, make sure another mode has been
		// selected as well (cdm doesn't like the "naked none")
		$tmp = array();
		foreach ($this->getTerms() as $t) {
			$tmp[] = $t->getMode();
		}
		$tmp = array_unique($tmp);
		if (in_array("none", $tmp) && count($tmp) == 1) {
			throw new DMIllegalArgumentException(
				DMLocalizedString::getString("NAKED_NONE_OPERAND"));
		}
		// if a proximity int is supplied, make sure both strings exist
		if ($this->getProximity() > 0) {
			$i = 0;
			foreach ($this->input_terms as $t) {
				if (strlen($t->getString()) > 0) {
					$i++;
				}
			}
			if ($i < 2) {
				throw new DMIllegalArgumentException(
					DMLocalizedString::getString("MISSING_PROXIMITY_STRING"));
			}
		}

		return true;
	}

}
